Google Script Running Extremely slow - google-apps-script

I have written a google script attached to my google sheet. It's been working well so far apart from today when it's been extremely slow. Does anyone have any idea why?
var ssID = "1cpR6AVVpk9TF4_I38IFYPPOqk-_bSROHgVVYdaXLXOI";
var formID = "14foKaEoUA_lhDmJTJExwQ5vavsGJShWm_x1kaANLhwU";
var wsData = SpreadsheetApp.openById(ssID).getSheetByName("car_number");
var form = FormApp.openById(formID);
function main(){
var labels = wsData.getRange(1,1,1,wsData.getLastColumn()).getValues()[0];
labels.forEach(function(label,i){
var options = wsData.getRange(2,i + 1,wsData.getLastRow()-1,1).getValues().map(function(o) { return o[0]}).filter(function(o){return o !== ""});
updateDropdownUsingTitle(label,options);
});
}
function updateDropdownUsingTitle(title,values) {
var items = form.getItems();
var titles = items.map(function(item){
return item.getTitle();
});
var pos = titles.indexOf(title);
if (pos !== -1) {
var item = items[pos];
var ItemID = item.getId();
updateDropdown(ItemID,values);
}
}
function updateDropdown(id,values) {
var item = form.getItemById(id);
item.asListItem().setChoiceValues(values);
}

Try getting all values using batch operations, and use the minimum of API calls in the loop, like this:
function main() {
// spreadsheet
const ssID = '1cpR6AVVpk9TF4_I38IFYPPOqk-_bSROHgVVYdaXLXOI';
const sheet = SpreadsheetApp.openById(ssID).getSheetByName('car_number');
const data = sheet.getDataRange().getValues();
const labels = data.shift();
const firstColumn = data.map(row => row[0]);
// form
const formID = '14foKaEoUA_lhDmJTJExwQ5vavsGJShWm_x1kaANLhwU';
const form = FormApp.openById(formID);
const items = form.getItems();
const itemIds = items.map(item => item.getId());
const itemTitles = items.map(item => item.getTitle());
// action
labels.forEach((label, rowIndex) => {
const options = firstColumn.filter((option, optionIndex) => optionIndex >= rowIndex && option !== '');
updateDropdownUsingTitle_(items, itemTitles, label, options);
});
}
function updateDropdownUsingTitle_(items, itemTitles, title, values) {
const pos = itemTitles.indexOf(title);
if (pos !== -1) {
updateDropdown_(items[pos], values);
}
}
function updateDropdown_(item, values) {
item.asListItem().setChoiceValues(values);
}
With this pattern, the only API calls in the loop are in item.asListItem().setChoiceValues(values).

Related

This is a Google App Script question dealing with two issues, one of speed and the other how to handle a method

The spreadsheet has 14 locations and that list will grow over time. Each location is on its own sheet. This is my variable declaration:
function locationSearch(searchTerm, selectedLocation) {
if (searchTerm == '') { return };
const ss = SpreadsheetApp.getActive();
searchResults = [];
const schoolLocation = [
"ALTUS_ACADEMY", "ANNUNCIATA", "EPIPHANY", "GRACE", "HOLY_FAMILY",
"INNOVATIVE_SCHOLARS", "ISLAMIC_CC_OF_IL", "JOSEPHINUM_HS",
"MATERNITY_BVM", "POPE_FRANCIS", "PROVIDENCE", "PUI_TAK", "ST_AILBE",
"ST_BORGIA", "ST_ELIZABETH", "ST_PAUL"
];
const schoolALTUSACADEMY = ss.getSheetByName('ALTUS_ACADEMY');
const schoolANNUNCIATA = ss.getSheetByName('ANNUNCIATA');
const schoolEPIPHANY = ss.getSheetByName('EPIPHANY');
const schoolGRACE = ss.getSheetByName('GRACE');
const schoolHOLYFAMILY = ss.getSheetByName('HOLY_FAMILY');
const iNNOVATIVESCHOLARS = ss.getSheetByName('INNOVATIVE_SCHOLARS');
const schoolISLAMICCCOFIL = ss.getSheetByName('ISLAMIC_CC_OF_IL');
const schoolJOSEPHINUMHS = ss.getSheetByName('JOSEPHINUM_HS');
const schoolPOPE_FRANCIS = ss.getSheetByName('POPE_FRANCIS');
const schoolMATERNITYBVM = ss.getSheetByName('MATERNITY_BVM');
const schoolPROVIDENCE = ss.getSheetByName('PROVIDENCE');
const schoolPUITAK = ss.getSheetByName('PUI_TAK');
const schoolSTAILBE = ss.getSheetByName('ST_AILBE');
const schoolSTBORGIA = ss.getSheetByName('ST_BORGIA');
const schoolSTELIZABETH = ss.getSheetByName('ST_ELIZABETH');
const schoolSTPAUL = ss.getSheetByName('ST_PAUL');
var schALTUSACADEMY = schoolALTUSACADEMY.getDataRange().getValues().slice(1);
var schANNUNCIATA = schoolANNUNCIATA.getDataRange().getValues().slice(1);
var schEPIPHANY = schoolEPIPHANY.getDataRange().getValues().slice(1);
var schGRACE = schoolGRACE.getDataRange().getValues().slice(1);
var schHOLYFAMILY = schoolHOLYFAMILY.getDataRange().getValues().slice(1);
var schiNNOVATIVESCHOLARS = iNNOVATIVESCHOLARS.getDataRange().getValues().slice(1);
var schISLAMICCCOFIL = schoolISLAMICCCOFIL.getDataRange().getValues().slice(1);
var schJOSEPHINUMHS = schoolJOSEPHINUMHS.getDataRange().getValues().slice(1);
var schMATERNITYBVM = schoolMATERNITYBVM.getDataRange().getValues().slice(1);
var schPOPEFRANCIS = schoolPOPE_FRANCIS.getDataRange().getValues().slice(1);
var schPROVIDENCE = schoolPROVIDENCE.getDataRange().getValues().slice(1);
var schPUITAK = schoolPUITAK.getDataRange().getValues().slice(1);
var schSTAILBE = schoolSTAILBE.getDataRange().getValues().slice(1);
var schSTBORGIA = schoolSTBORGIA.getDataRange().getValues().slice(1);
var schSTELIZABETH = schoolSTELIZABETH.getDataRange().getValues().slice(1);
var schSTPAUL = schoolSTPAUL.getDataRange().getValues().slice(1);
var schLocation = [
schALTUSACADEMY, schANNUNCIATA, schEPIPHANY, schGRACE, schHOLYFAMILY,
schiNNOVATIVESCHOLARS, schISLAMICCCOFIL, schJOSEPHINUMHS, schMATERNITYBVM,
schPOPEFRANCIS, schPROVIDENCE, schPUITAK, schSTAILBE, schSTBORGIA, schSTELIZABETH,
schSTPAUL
];
var all_Locations = schALTUSACADEMY.concat(schANNUNCIATA, schEPIPHANY, schGRACE, schHOLYFAMILY,
schiNNOVATIVESCHOLARS, schISLAMICCCOFIL, schJOSEPHINUMHS, schMATERNITYBVM, schPOPEFRANCIS,
schPROVIDENCE, schPUITAK, schSTAILBE, schSTBORGIA, schSTELIZABETH, schSTPAUL);
//var loc = lSearch(searchTerm);
Is there a better way of getting these sheets into memory?
The second problem getting this method to return the correct value
for (var i = 0; i < dataBase.length; i++) { //[x] select the row to be searchedfor (var x = 0; x < dataBase[0].length; x++) {
if (dataBase[i][x] == searchTerm) { //[x] search the row for search term
var item = {};
item.entryMadeby = dataBase[i][1];
item.checkinDate = dataBase[i][2];
item.productCode = dataBase[i][3];
item.description = dataBase[i][4];
item.location = function () {
**This portion of the code calls a function that return a location**
lSearch(searchTerm)
return
};**
item.checkoutdate = dataBase[i][6];
//Logger.log(item);
searchResults.push(item);
//Logger.log((i+1));
//console.log(item);
//console.log(item.location());
}
}
}
} else {for (var index = 0; index \< schLocation.length; index++) {dataBase = schLocation\[index\];
for (var i = 0; i < dataBase.length; i++) { //[x] select the row to be search
for (var x = 0; x < dataBase[0].length; x++) {
if (dataBase[i][x] == searchTerm) { //[x] search the row for search term
var item = {
'entryMadeby': dataBase[i][1],
'checkinDate': dataBase[i][2],
'productCode': dataBase[i][3],
'description': dataBase[i][4],
'location': function () {
lSearch(searchTerm)
return
},
'checkoutdate': dataBase[i][6]
};
searchResults.push(item);
//console.log(item.location());
}
}
}
}
}//console.log(searchResults);return searchResults;}
Obvisoly I am new to GAS, that said I have been reading, testing and youtubing to move this spreadsheet from excel to GAS. These are the last steps need. As you can see I attemping to reduce the number of times the spreadsheets gets called. Each sheet gets pull into an array before the array is loop not spreadsheet. This makes sense as a best practice but is thier a better way? The object called Item solve the problem of getting search results but the method is not working correctly. It return undefine always. Beside the method handling code is working.
just slow. The priority for me is getting method handling to work.
Is there a better way of getting these sheets into memory?
Use Array methods such as .map() to make the code more manageable, like this:
function test() {
const sheetNames = [
'ALTUS_ACADEMY', 'ANNUNCIATA', 'EPIPHANY', 'GRACE', 'HOLY_FAMILY',
'INNOVATIVE_SCHOLARS', 'ISLAMIC_CC_OF_IL', 'JOSEPHINUM_HS',
'MATERNITY_BVM', 'POPE_FRANCIS', 'PROVIDENCE', 'PUI_TAK', 'ST_AILBE',
'ST_BORGIA', 'ST_ELIZABETH', 'ST_PAUL',
];
const data = mergeSheetData_(sheetNames);
console.log(data);
}
/**
* Gets sheets listed in sheetNames and merges their data into a 2D array
* where every row starts with the name of the sheet it came from.
*
* #param {String[]} sheetNames The names of the sheets to process.
* #return {String[][]} A 2D array that contains sheet names and data.
*/
function mergeSheetData_(sheetNames) {
const ss = SpreadsheetApp.getActive();
const data3D = sheetNames
.map(sheetName => ss
.getSheetByName(sheetName)
.getDataRange()
.getValues()
.slice(1)
.map(row => [sheetName, ...row])
);
let result = [];
data3D.forEach(table => result = result.concat(table));
return result;
}

how to copy a range with groups information?

I copy "range" from one sheet to another using range.copyTo(..) function, it doesnt copy group information
var destSheet = some_sheet;
var sourceRange=SpreadsheetApp.getActiveSheet().getRange("A1:H10");
sourceRange.copyTo(destSheet.getRange(1,1));
Values, format, formula are copied. Not groups
Issue and workaround:
In the current stage, it seems that the dimension groups cannot be directly copied by copyTo method. So, in order to copy the dimension groups, it is required to retrieve the dimension groups and put them.
When Sheets API is used, the dimension groups can be retrieved as an object. Using this, in this answer, I would like to propose to use Sheets API. When this is reflected in your script, it becomes as follows.
Modified script:
Before you use this script, please enable Sheets API at Advanced Google services. And, please set your destination sheet name.
function deleteDimensionGroups_(sheetId, sheet) {
var obj = [...(sheet.rowGroups || []), ...(sheet.columnGroups || [])];
if (obj.length == 0) return [];
return obj
.sort((a, b) => a.depth < b.depth ? 1 : -1)
.map(o => {
o.range.sheetId = sheetId;
delete o.depth;
return { deleteDimensionGroup: o };
});
}
// Please run this function.
function main() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ssId = ss.getId();
var srcSheet = ss.getActiveSheet();
var dstSheet = ss.getSheetByName("Sheet2"); // Please set your destination sheet name.
var sourceRange = srcSheet.getRange("A1:H10");
sourceRange.copyTo(dstSheet.getRange(1, 1));
// Copy dimension groups.
var { sheets } = Sheets.Spreadsheets.get(ssId, { ranges: [srcSheet.getSheetName(), dstSheet.getSheetName()] });
var sheet = sheets[0];
var obj = [...(sheet.rowGroups || []), ...(sheet.columnGroups || [])];
if (obj.length == 0) return;
var sheetId = dstSheet.getSheetId();
var reqs = deleteDimensionGroups_(sheetId, sheets[1]);
var requests = [...reqs, ...obj
.sort((a, b) => a.depth > b.depth ? 1 : -1)
.map(o => {
o.range.sheetId = sheetId;
delete o.depth;
return { addDimensionGroup: o };
})];
Sheets.Spreadsheets.batchUpdate({ requests }, ssId);
}
Note:
In this sample script, the existing dimension groups are deleted from the destination sheet and the new dimension groups are copied. If you don't want to delete the dimension groups in the destination sheet, please modify the above script as follows.
From:
var reqs = deleteDimensionGroups_(sheetId, sheets[1]);
To:
var reqs = [];
References:
Method: spreadsheets.batchUpdate
AddDimensionGroupRequest
Added:
From your following reply,
It's full of informations. Trying to adapt it and create a copyToWithGroup(sourceRange, destRange).. not so easy at my level. But I have materials in your answer to work with.
If you want to limit the range using var sourceRange = srcSheet.getRange("A1:H10");, how about the following sample script?
Sample script:
function deleteDimensionGroups_(sheetId, obj) {
if (obj.length == 0) return [];
return obj
.sort((a, b) => a.depth < b.depth ? 1 : -1)
.map(o => {
o.range.sheetId = sheetId;
delete o.depth;
return { deleteDimensionGroup: o };
});
}
// Please run this function.
function main() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ssId = ss.getId();
var srcSheet = ss.getActiveSheet();
var dstSheet = ss.getSheetByName("Sheet2"); // Please set your destination sheet name.
var sourceRange = srcSheet.getRange("A1:H10"); // In this sample, this range is used.
sourceRange.copyTo(dstSheet.getRange(1, 1));
// Copy dimension groups in "sourceRange".
var startRowIndex = sourceRange.getRow() - 1;
var endRowIndex = startRowIndex + sourceRange.getNumRows();
var startColumnIndex = sourceRange.getColumn() - 1;
var endColumnIndex = startColumnIndex + sourceRange.getNumColumns();
var { sheets: [src, dst] } = Sheets.Spreadsheets.get(ssId, { ranges: [srcSheet.getSheetName(), dstSheet.getSheetName()] });
var check = f => f.filter(({ range }) => {
if (range.dimension == "ROWS" && range.startIndex >= startRowIndex && range.endIndex <= endRowIndex) {
return true;
} else if (range.dimension == "COLUMNS" && range.startIndex >= startColumnIndex && range.endIndex <= endColumnIndex) {
return true;
} else {
return false;
}
});
var [src2, dst2] = [src || [], dst || []].map(e => {
var temp = [];
if (e.rowGroups && e.rowGroups.length > 0) temp = [...temp, ...check(e.rowGroups)];
if (e.columnGroups && e.columnGroups.length > 0) temp = [...temp, ...check(e.columnGroups)];
return temp;
});
if (src2.length == 0) return;
var sheetId = dstSheet.getSheetId();
var reqs = deleteDimensionGroups_(sheetId, dst2);
var requests = [...reqs, ...src2
.sort((a, b) => a.depth > b.depth ? 1 : -1)
.map(o => {
o.range.sheetId = sheetId;
delete o.depth;
return { addDimensionGroup: o };
})];
Sheets.Spreadsheets.batchUpdate({ requests }, ssId);
}

How to display info from other sheets, specific to text in cells

Our company works at different properties repairing appliances, I would like to build a database to search up the information on each appliance at specific properties and in their specific apt/units, I created a form to start this process, but I need help with some complex coding.
I first created a box for the property, then I created an "Apt/Unit" box. The idea is when I select a property, the units tied to that property are shown in dropdown/type searchable list in the Apt/Unit box.
I then created an "Appliance type" box. The idea is when the "Apt/Unit" is selected, it will display the dropdown/type searchable list of the appliances tied to that specific "Apt/Unit".
Then I created boxes for the info for the appliance (Brand, Model #, Serial #, & Color), this is a bit more self-explanatory - once the appliance type is selected, it will display the respective information for each box for that appliance.
Here's the link to the Google sheet: https://docs.google.com/spreadsheets/d/1JZhEYjk5xVN3uOc_Ucb8HFr6d96XQ2Q_ehAd-d_o0ME/edit?usp=sharing
Any help is appreciated!
non-scripted solution:
=IFERROR({INDEX(IFERROR(Data!A1:G1/0)); Data!A1:G1; QUERY({Data!A2:G}, "where 1=1 "&
IF(C10="",,"and lower(Col1) contains '"&LOWER(C10)&"'")&
IF(C12="",,"and Col2 = "&C12)&
IF(C14="",,"and lower(Col3) contains '"&LOWER(C14)&"'")&
IF(C16="",,"and lower(Col4) contains '"&LOWER(C16)&"'")&
IF(C18="",,"and lower(Col5) contains '"&LOWER(C18)&"'")&
IF(C20="",,"and lower(Col6) contains '"&LOWER(C20)&"'")&
IF(C22="",,"and lower(Col7) contains '"&LOWER(C22)&"'"), 0)}, {"";"no data"})
demo sheet
Here is third variant of the script:
// global variables
var SS = SpreadsheetApp.getActiveSpreadsheet();
var SHEET_USERFACE = SS.getSheetByName('Userface');
var SHEET_DATA = SS.getSheetByName('Data');
function onLoad() { reset() }
function reset() {
SS.toast('Please wait...');
SHEET_USERFACE.getRange('c9:c21').clearContent();
SHEET_USERFACE.getRange('c9:c13').clearDataValidations();
var obj = make_obj_from_data();
update_menu_prop(obj);
update_menu_unit(obj);
update_menu_type(obj);
SS.toast('The sheet has been reset');
}
function onEdit(e) {
if (e.range.getSheet().getName() != 'Userface') return;
if (e.range.columnStart != 3) return;
// Property menu
if (e.range.rowStart == 9) {
e.source.toast('Please, wait...');
SHEET_USERFACE.getRange('c11:c21').clearContent();
SHEET_USERFACE.getRange('c11:c13').clearDataValidations();
var obj = make_obj_from_data();
update_menu_unit(obj);
update_menu_type(obj);
e.source.toast('The sheet has been updated');
}
// Apt/Unit menu
if (e.range.rowStart == 11) {
e.source.toast('Please, wait...');
SHEET_USERFACE.getRange('c13:c21').clearContent();
SHEET_USERFACE.getRange('c13').clearDataValidations();
var obj = make_obj_from_data();
update_menu_type(obj);
e.source.toast('The sheet has been updated');
}
// Applicance type menu
if (e.range.rowStart == 13) {
e.source.toast('Please, wait...');
SHEET_USERFACE.getRange('c15:c21').clearContent();
var obj = make_obj_from_data();
update_brand_model_serial_color(obj);
e.source.toast('The sheet has been updated');
}
}
function make_obj_from_data() {
var data = SHEET_DATA.getDataRange().getValues().slice(1);
var obj = {};
for (let row of data) {
var [prop, unit, type, ...etc] = row;
try {
obj[prop][unit][type] = etc;
}
catch(e) {
try {
obj[prop][unit] = {}; obj[prop][unit][type] = etc;
}
catch(e) {
obj[prop] = {}; obj[prop][unit] = {}; obj[prop][unit][type] = etc;
}
}
}
return obj;
}
function update_menu_prop(obj) {
var cell = SHEET_USERFACE.getRange('c9');
try {
var list = Object.keys(obj);
set_data_validation(cell, list);
} catch(e) {
console.log('update_menu_prop(obj)');
console.log(e);
}
}
function update_menu_unit(obj) {
var prop = SHEET_USERFACE.getRange('c9').getValue();
var cell = SHEET_USERFACE.getRange('c11');
try {
var list = Object.keys(obj[prop]);
set_data_validation(cell, list);
} catch(e) {
console.log('update_menu_unit(obj)');
console.log(e);
}
}
function update_menu_type(obj) {
var prop = SHEET_USERFACE.getRange('c9').getValue();
var unit = SHEET_USERFACE.getRange('c11').getValue();
var cell = SHEET_USERFACE.getRange('c13');
try {
var list = Object.keys(obj[prop][unit]);
set_data_validation(cell, list);
if (list.length == 1) update_brand_model_serial_color(obj)
} catch(e) {
console.log('update_menu_type(obj)');
console.log(e);
}
}
function update_brand_model_serial_color(obj) {
var [prop,,unit,,type] = SHEET_USERFACE.getRange('c9:c13').getValues();
try {
var [brand, model, serial, color] = obj[prop][unit][type];
var arr = [[brand],[''],[model],[''],[serial],[''],[color]];
SHEET_USERFACE.getRange('c15:c21').setValues(arr);
} catch(e) {
console.log('update_brand_model_serial_color(obj)');
console.log(e);
}
}
function set_data_validation(cell, list) {
var rule = SpreadsheetApp.newDataValidation().requireValueInList(list).build();
cell.setDataValidation(rule);
// put the value in the cell if there is just one element in the list
if (list.length == 1) cell.setValue(list[0]);
}
Here is my sheet.
It works about that way as it does any similar interface. You select the first menu and it changes data validation for the second menu and cleans the third menu. Then you select the second menu and it changes the third one. As soon as you change the third menu it fills the rest fields.
Since you're using just the three menus and they supposed to be changed step by step I decided to 'hardcode' them. It's not the best practice and there can be problems if/when you decide to change the functionality. But for this particular case I think the 'hardcoding' is forgivable. It works relatively fast and the code is relatively readable.
Just for fun I've made it. But this is overkill:
// global variables
var SS = SpreadsheetApp.getActiveSpreadsheet();
var SHEET_USERFACE = SS.getSheetByName('Userface');
var SHEET_DATA = SS.getSheetByName('Data');
function onLoad() { reset() }
function onEdit(e) {
if (e.range.getSheet().getName() != 'Userface') return;
if (e.range.columnStart != 3) return;
if (![9,11,13,15,17,19,21].includes(e.range.rowStart)) return;
e.source.toast('Please, wait...');
set_filter(e.range.offset(0,-1).getValue(), e.value);
set_all_menus();
e.source.toast('The sheet has been updated');
}
function reset() {
SS.toast('Please wait...');
try { SHEET_DATA.getFilter().remove() } catch(e) {}
SHEET_USERFACE.getRange('c9:c21').clearContent().clearDataValidations();
set_all_menus();
SS.toast('The sheet has been updated');
}
function set_all_menus() {
var data = SHEET_DATA.getDataRange().getDisplayValues().filter((_,i) => !SHEET_DATA.isRowHiddenByFilter(i+1));
set_menu(data, 'b9', 'c9');
set_menu(data, 'b11', 'c11');
set_menu(data, 'b13', 'c13');
set_menu(data, 'b15', 'c15');
set_menu(data, 'b17', 'c17');
set_menu(data, 'b19', 'c19');
set_menu(data, 'b21', 'c21');
}
function set_menu(data, title, cell) {
var menu_title = SHEET_USERFACE.getRange(title).getValue();
var menu_cell = SHEET_USERFACE.getRange(cell);
var col_index = data[0].indexOf(menu_title);
var menu_list = [...new Set([...data.map(e => e[col_index])])].slice(1);
var menu_rule = SpreadsheetApp.newDataValidation().requireValueInList(menu_list).build();
menu_cell.setDataValidation(menu_rule);
}
function set_filter(column_title, value) {
// get all the data and col index
var [header, ...data] = SHEET_DATA.getDataRange().getValues();
var col_index = header.indexOf(column_title);
// unhide all values of the given column
var clear = SpreadsheetApp.newFilterCriteria().setHiddenValues([]).build();
var range = SHEET_DATA.getDataRange();
var filter = range.getFilter() || range.createFilter()
filter.setColumnFilterCriteria(col_index+1, clear);
// get the values to hide
var col_data = data.map(e => e[col_index]);
var filtered = col_data.filter( (e, i) => e != value && SHEET_DATA.isRowHiddenByFilter(i+1) );
var to_hide = col_data.filter( e => e != value );
var hidden = [...new Set([...filtered, ...to_hide])];
// hide the values with the filter
var criteria = SpreadsheetApp.newFilterCriteria().setHiddenValues(hidden).build();
var range = SHEET_DATA.getDataRange();
var filter = range.getFilter() || range.createFilter()
filter.setColumnFilterCriteria(col_index+1, criteria);
}
Here is the sheet.
It works quite slow. I'd propose to use the native filters instead. Basically the script turns on and off the filters an changes data validation for the dropdown menus respectively.
Update
Here another version of the script. It works much faster but it uses the 'helper sheet' to store temporary data (the filtered table). You can hide the 'helper sheet' if you want.
// global variables
var SS = SpreadsheetApp.getActiveSpreadsheet();
var SHEET_USERFACE = SS.getSheetByName('Userface');
var SHEET_DATA = SS.getSheetByName('Data');
var SHEET_HELPER = SS.getSheetByName('Helper'); // the hidden sheet with temp data
var PROPERTY_LIST = [...new Set(SHEET_DATA.getRange('a2:a').getValues().flat())]; // 'Property' list
var DATA_OBJ = {};
function onLoad() { reset() }
function onEdit(e) {
var {range, source, value} = e;
if (range.getSheet().getName() != 'Userface') return;
if (range.columnStart != 3) return;
if (![9,11,13,15,17,19,21].includes(range.rowStart)) return;
source.toast('Please, wait...');
// reset whenever the first menu is changing
if (range.rowStart == 9) {
reset();
source.getRange('c9').setValue(value);
}
var col_header = range.offset(0,-1).getValue();
update_sheet_helper(col_header, value);
update_all_dropdown_menus();
source.toast('The sheet has been updated');
}
function reset() {
SS.toast('Please wait...');
// copy data from SHEET_DATA to SHEET_HELPER
SHEET_USERFACE.getRange('c9:c21').clearContent().clearDataValidations();
SHEET_DATA.getDataRange().copyTo(SHEET_HELPER.clearContents().getRange(1,1));
update_data_obj();
update_all_dropdown_menus();
SS.toast('The sheet has been updated');
}
// make DATA_OBJECT from SHEET_HELPER
function update_data_obj() {
DATA_OBJ = {};
var [header, ...data] = SHEET_HELPER.getDataRange().getValues();
for (let i in header) DATA_OBJ[header[i]] = data.map(e => e[i]);
DATA_OBJ['Property'] = PROPERTY_LIST; // let 'Property' list will be full always
}
// remove from SHEET_DATA_HELPER all the rows
// that have no given value in column with given title
function update_sheet_helper(col_title, value) {
var [header, ...data] = SHEET_HELPER.getDataRange().getValues();
var col_index = header.indexOf(col_title);
data = data.filter(k => k[col_index] == value);
var table = [header, ...data];
SHEET_HELPER.clearContents().getRange(1,1,table.length, table[0].length).setValues(table);
update_data_obj();
}
function update_all_dropdown_menus() {
SHEET_USERFACE.getRange('b9:c21').getValues().forEach((row,i) => {
if (row[0] != '') set_data_validation(DATA_OBJ[row[0]], 'c' + (i+9));
});
function set_data_validation(data, cell_address) {
var menu_list = [...new Set([...data])]; // remove duplicates from the array
var menu_rule = SpreadsheetApp.newDataValidation().requireValueInList(menu_list).build();
var cell_range = SHEET_USERFACE.getRange(cell_address)
cell_range.setDataValidation(menu_rule);
if (menu_list.length == 1) cell_range.setValue(menu_list[0]);
}
}
The sheet is here.

Google App Script to Search data from multiple files and displaying multiple row

I want users to search for their info from multiple sheet files and display multiple results boxes(row) but currently it's searching from one sheet and displaying only the first result(row) and not displaying the rest results.
What do I need to change in the script?
function submitData(obj){
var ss = SpreadsheetApp.openById("SHEETID");
var sheet = ss.getSheetByName("SHEETNAME");
var flag = 1 ;
var lr = sheet.getLastRow();
for(var i = 1;i <= lr;i++){
var id = sheet.getRange(i, 1).getValue();
if(id == obj){
flag = 0;
var B = sheet.getRange(i, 2).getValue();
var C = sheet.getRange(i, 3).getDisplayValue();
var data ="<div class="card"><h3>Info1:"+B+"</h3><p>Info2:"+C+"</p></div>";
return data;
}
}
if(flag==1){
var data = '<div class="carderror">Data not found</div>';
return data;
}
index.html
<input id="id" class="form-control">
<button type="submit" onclick="info()"> search</button>
<div id="result"></div>
<script>
function info () {
let obj = document.querySelector('#id').value;
let updateLocation = document.querySelector('#result');
updateLocation.innerHTML = "Searching...";
function onFailure(error){
let warning = error;
updateLocation.innerHTML = warning;
};
function onSuccess(response){
let result = response;
updateLocation.innerHTML = result;
};
google.script.run.withFailureHandler(onFailure)
.withSuccessHandler(onSuccess)
.submitData(obj);
};
</script>
You're only opening a single sheet file in your example. If you want to search multiple you'll have to specify which files you want to open and loop through them. My example below assumes all the files have the same sheet name.
Right now you're returning the first matched result, which is causing your search function to exit prematurely. To display multiple results you can push the matched lines to an array, then return that array once all the scanning is complete.
function submitData(obj) {
//specify what sheet IDs you want to search
var sheetNames = ['foo', 'bar', 'bla']
//create an array to push results to
var inject = []
for (var i = 0; i < sheetNames.length; i++) {
var sheetName = sheetNames[i]
var ss = SpreadsheetApp.openById(sheetName);
var sheet = ss.getSheetByName("SHEETNAME");
var flag = 1;
var lr = sheet.getLastRow();
for (var i = 1; i <= lr; i++) {
var id = sheet.getRange(i, 1).getValue();
if (id == obj) {
flag = 0;
var B = sheet.getRange(i, 2).getValue();
var C = sheet.getRange(i, 3).getDisplayValue();
var data = "<div class='card'><h3>Info1:" + B + "</h3><p>Info2:" + C + "</p></div>";
//push result to array
inject.push(data)
}
}
}
if (flag == 1) {
var data = '<div class="carderror">Data not found</div>';
return data;
} else {
//return joined array as string
return inject.join()
}
}
Multi-spreadsheet search
function MultiSearch() {
const ss = SpreadsheetApp.getActive();
const idsh = ss.getSheetByName("ssids");
const rsh = ss.getSheetByName("Results");
rsh.getRange(2,1,rsh.getLastRow() - 1, rsh.getLastColumn()).clearContent()
const ids = idsh.getRange(2,1,idsh.getLastRow() - 1,idsh.getLastColumn()).getValues().filter(r => r[2] == "TRUE").map(r => r[1]);
const r = SpreadsheetApp.getUi().prompt("Search Dialog","Enter Search String",SpreadsheetApp.getUi().ButtonSet.OK_CANCEL);
if(r.getSelectedButton() == SpreadsheetApp.getUi().Button.OK && r.getResponseText().length > 0) {
ids.forEach(id => {
let ss = SpreadsheetApp.openById(id);
let tf = ss.createTextFinder(r.getResponseText()).findAll();
tf.forEach(f =>{
rsh.appendRow([ss.getName,f.getSheet().getName(),f.getRow(),f.getColumn()]);
})
})
}
}
ssids Sheet:
Results Sheet:

Search a Match and return value google sheets

I am trying to write a code that will look for a match (2 criteria) and will return a value.
Here is sample spreadsheet: Sample
What I want to achieve is:
Read Product and Stage data from the Result(段取)sheet
Find a match in DataBase sheet
Get the Time (cell next to stage) value and copy it to the Result sheet
DataBase is constant and it won't get change but the Result (product and stage) will change everyday.
Please think of it as of production process. In database you have written whole process and in the Result sheet you have current status of the production. I want to know how much time left to get final product and also I want to know time of every stage.
In the spreadsheet you have ExampleResult which is showing the result I want to achieve.
So I guess it has to be something which will look for a match first in Column (for a product) and then in a Row (for a stage). Original data has around 40 columns and 15000 rows...
Do you have any idea how it can be solved?
This is function I wrote so far but don't know how to move it further...
function findmatch(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var srcSheet = ss.getSheetByName("DataBase(段取)");
var srcValues = srcSheet.getRange(2, 1, srcSheet.getLastRow(), srcSheet.getLastColumn()).getValues();
var productlist = srcValues.map(function(r){return r[0]});
var stage = srcValues.map(function(r){return r[2]});
var arr1 = Array.of(productlist);
var arr2 = Array.of(stage);
var position = arr1.indexOf(productlist);
if (position >-1){
return arr2[position];
}else {
return 'N/A';
}
Logger.log(srcValues)
}
and this:
function match(){
const ss = SpreadsheetApp.getActiveSpreadsheet();
var srcSheet = ss.getSheetByName("ルーチンデータ(加工)");
var srcValues = srcSheet.getRange(2, 1, srcSheet.getLastRow()-1, srcSheet.getLastColumn()).getValues();
var lol = Array.of(srcValues);
var filtered = lol.filter(item => item);
const dstSheet = ss.getSheetByName("結果(段取)");
const dstValues = dstSheet.getRange(2, 1, dstSheet.getLastRow()-1, dstSheet.getLastColumn()).getValues();
var loldst = Array.of(dstValues);
var filtered1 = loldst.filter(item => item);
let map = {};
filtered.forEach(i => map[i] = false);
filtered1.forEach(i => map[i] === false && (map[i] = true));
let jsonArray = Object.keys(map).map(k => ({ name: k, matched: map[k] }));
Logger.log(jsonArray);
}
I will be very grateful for any advice.
Regards,
Pimo
So I found a solution to my problem.
Maybe it will be helpful for other as well
function makeArray(srcValues){
const maxSrcValues = srcValues.length;
const srcAry = [];
for(let i = 0; i< maxSrcValues; i++){
let maxCol = srcValues[i].length;
let rowAry = [];
for(let j = 2; j<maxCol; j = j+2 ){
if(srcValues[i][j]){
let t = 0
if(srcValues[i][j + 1]){
t = parseFloat(srcValues[i][j + 1]);
}
rowAry.push({'stage': srcValues[i][j], 'time': t});
}
}
srcAry.push({'product': srcValues[i][0], 'stages': rowAry});
}
return srcAry;
}
function getTime(product, stage, srcAry){
const maxSrcAry = srcAry.length;
for(let i= 0; i<maxSrcAry; i++ ){
if(product == srcAry[i].product){
let maxRow = srcAry[i].stages.length;
for(let j=0; j<maxRow;j++){
if(stage == srcAry[i].stages[j].stage){
return parseFloat(srcAry[i].stages[j].time);
}
}
}
}
return 0;
}
function getDbProductByStages(product, stagesList,srcAry){
if(stagesList === null){
return false;
}
const maxSrcAry = srcAry.length;
const firstKey = stagesList[0].stage;
let stagesCnt = 0
for(let i= 0; i<maxSrcAry; i++ ){
if(product == srcAry[i].product){
stagesCnt = srcAry[i].stages.length
for(let j=0; j<stagesCnt;j++){
if(firstKey == srcAry[i].stages[j].stage){
if(isSameProduct(j, srcAry[i].stages, stagesList) === true){
return srcAry[i].stages;
}
}
}
}
}
return false;
}
function isSameProduct(idx, srcStages, destStages) {
const maxDstStg = destStages.length - 1;
const maxSrcStg = srcStages.length - idx;
if(maxDstStg > maxSrcStg){
return false;
}
for(let i = 0; i<maxDstStg; i++){
if(destStages[i].stage != srcStages[i + idx].stage){
return false;
}
}
return true;
}
function getStageTime(firstStg, idx, stage, dbProduct){
const maxCnt = dbProduct.length
for(let i = 0; i < maxCnt; i++){
if(firstStg == dbProduct[i].stage){
if(dbProduct[i + idx] && dbProduct[i + idx].stage == stage){
return dbProduct[i + idx].time;
}
break;
}
}
return 0;
}
function doProcess() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
// 1. Retrieve the source values from database sheet.
const srcSheet = ss.getSheetByName("DataBase(段取)");
const srcValues = srcSheet.getRange(2, 1, srcSheet.getLastRow(), srcSheet.getLastColumn()).getValues();
const srcAry = makeArray(srcValues);
// 3. Retrieve the source values from Report sheet.
const dstSheet = ss.getSheetByName("Result(段取)");
const dstValues = dstSheet.getRange(2, 1, dstSheet.getLastRow() - 1, dstSheet.getLastColumn()).getValues();
// 4. Create an array for putting to Spreadsheet using the created object.
const destAry = makeArray(dstValues);
const maxDestAry = destAry.length;
const result = [];
for(let i= 0; i<maxDestAry; i++ ){
let row = [];
let rowPartial = [];
row.push(destAry[i].product);
let timeLeft = 0;
let timeTmp = 0;
let maxRow = destAry[i].stages.length;
let dbProduct = getDbProductByStages(destAry[i].product, destAry[i].stages,srcAry);
for(let j=0; j<maxRow;j++){
rowPartial.push(destAry[i].stages[j].stage);
if(dbProduct){
timeTmp = getStageTime(destAry[i].stages[0].stage,j,destAry[i].stages[j].stage, dbProduct);
}else{
timeTmp = 0;
}
rowPartial.push(timeTmp);
timeLeft = timeLeft + timeTmp;
}
row.push(timeLeft);
for(let k=0;k<rowPartial.length;k++) {
row.push(rowPartial[k]);
}
result.push(row);
}
const maxLength = result.length;
let maxColl = 0;
for(let i=0; i<maxLength;i++){
if(maxColl < result[i].length){
maxColl = result[i].length;
}
}
for(let i=0; i<maxLength;i++){
if(result[i].length < maxColl){
let diff = maxColl - result[i].length;
for(let j=0;j<diff;j++){
result[i].push('');
}
}
}
const wynikSheet = ss.getSheetByName("wynik");
wynikSheet.getRange(2,1, result.length, result[0].length).setValues(result);
}
Just run the doProcess function