How can I gather all data from my sheets? - google-apps-script

I have a Google Spreadsheet with a lot of sheets containing data, it goes between 1-2 to 40-50 rows per sheet.
I must also note that I'm not a programmer and I am working on this based on Google results.
My goal is to have a "Master" sheet that collects the data from all other sheets and a "Filter" sheet, where I can filter all that "Master" data based on certain values.
I have been able to get it "sort of" working but due to the amount of data, the script times-out most of the time and the main problem is, column headers are not always in the same order and I get data that is hard to filter and work on.
Here is what has been working so far:
const URL_LIST_SHEET_NAME = "URL_LIST!";
const FILTER_SHEET = "Filter";
const MASTER_NAME = "Master";
const TEMPLATE_NAME = "Template";
function GET_DATA() {
const dataSheets = SpreadsheetApp.getActiveSpreadsheet()
.getSheets()
for (let s in dataSheets) {
let sheetNm = dataSheets[s].getName();
// Should skip sheets that I don't need, to reduce the time but not really working ???
if (sheetNm === FILTER_SHEET || sheetNm === URL_LIST_SHEET_NAME || sheetNm === MASTER_NAME || sheetNm === TEMPLATE_NAME) { continue; }
const ranges = dataSheets
.map(info => info.getRange("A2:F30")); //if I put info.getDataRange() here it doesn't work
return ranges
.reduce((result, range) => result.concat(range.getValues()), []);
}
}
What I've been trying to do is to get data sorted based on header rows by combining different solutions I found.
So far it's not working and even when I get it to successfully execute there is nothing in the "Master" sheet. The most common error I get is "TypeError: sheets.getDataRange is not a function"
const URL_LIST_SHEET = "URL_LIST!";
const FILTER_SHEET = "Filter";
const MASTER_NAME_SHEET = "Master";
const TEMPLATE_NAME_SHEET = "Template";
const target_headers = ['firstName', 'companyName', 'BadLinkURL', 'DiscreditProofURL', 'email', 'Niche'];
// Headers I need to sort by, they are not always in the same columns and I need them to be for exporting
function GetColumnValues() {
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i=0; i<sheets.length; i++) {
for (let s in sheets) {
// Gets sheet name.
let sheetNm = sheets[s].getName();
// Skips sheets.
if (sheetNm === FILTER_SHEET || sheetNm === URL_LIST_SHEET || sheetNm === MASTER_NAME_SHEET || sheetNm === TEMPLATE_NAME_SHEET)
{ continue; }
const range = sheets[i].getDataRange(); //With varying data ranges, triming the completely blank rows is a must
const values = range.getValues();
const headers = values.shift();
const columnIndex = headers.indexOf(target_headers);
const columnValues = values.map(row => row[columnIndex]);
return columnValues;
}
}
}
Considering I'm getting timeouts, can I process this in batches of ~30 sheets until all have been processed? The number of sheets in this workbook is 100+ and will only increase so I think that this will be a serious issue for execution time.

First problem I see in your GET_DATA function is that you have a nested a loop inside a loop... and it's the same loop (going over all the sheets). So you should do that only once.
Better try something like
const excluded = [
FILTER_SHEET,
URL_LIST_SHEET_NAME,
MASTER_NAME,
TEMPLATE_NAME
]
const RANGE = "A2:F30"
function GET_DATA() {
const dataSheets = SpreadsheetApp.getActiveSpreadsheet().getSheets()
const values = dataSheets
.filter(dataSheet => excluded.indexOf(dataSheet.getName()) === -1)
.map(info => info.getRange(RANGE))
.reduce((result, range) => result.concat(range.getValues()), []);
return values
}

The #ValLeNain's answer is correct, I would just add the same suggestion on the second function GetColumnValues(). You have 2x for-loop and mixing it, that's causing your error TypeError: sheets.getDataRange is not a function and probably not helping your executing time.
Modified code:
const URL_LIST_SHEET = "URL_LIST!";
const FILTER_SHEET = "Filter";
const MASTER_NAME_SHEET = "Master";
const TEMPLATE_NAME_SHEET = "Template";
const target_headers = ['firstName', 'companyName', 'BadLinkURL', 'DiscreditProofURL', 'email', 'Niche'];
// Headers I need to sort by, they are not always in the same columns and I need them to be for exporting
function GetColumnValues() {
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
//one for-loop removed
for (var i = 0; i < sheets.length; i++) {
let sheetNm = sheets[i].getName(); //modified
if (sheetNm === FILTER_SHEET || sheetNm === URL_LIST_SHEET || sheetNm === MASTER_NAME_SHEET || sheetNm === TEMPLATE_NAME_SHEET) {
continue;
}
var range = sheets[i].getDataRange();
var values = range.getValues();
var headers = values.shift();
var columnIndex = headers.indexOf(target_headers); //not tested
var columnValues = values.map(row => row[columnIndex]); //not tested
return columnValues;
}
}
Note:
Spreadsheet has 200 sheets limitation Spreadsheet limitations
You will face problems with executing time anyway with a file this large

Related

Moving rows to another sheet based on certain criteria in google apps script

I am working on creating a script that needs to push rows from one sheet of a google workbook to another based on how the row is categorized by an entry in another column. This needs to be also adaptable to have it push to a different google workbook in some cases in the future. I have tried multiple iterations of the following script and it will pull the rows over and then updated background colors, but it is just iterating through all the data and pulling over everything instead of just those rows with an "X" in the relevant column.
What I'd like it to do is pull only those on the "Feedback" tab which are not set to green as the cell color in column F, and those with an "X" in column F, then to have it set the cell to green once it has pulled so that it won't pull the same information next time I run the script. I want to then have it do the same thing using column G.
Here is a test doc I have been testing with.
https://docs.google.com/spreadsheets/d/1JLyEuVijQ8MvfOKrtbRD_YKmRDnTCxf7qCSw9Ggty_Y/edit#gid=384324173
This is the code I have currently:
function oneFeedback() {
var sss = SpreadsheetApp.getActiveSpreadsheet();
var ss = sss.getSheetByName("Feedback");
var s = ss.getSheetName();
var data = ss.getDataRange().getValues();
var bostab = sss.getSheetByName("1");
if(s !== "Feedback"){
Browser.msgBox("This option isn't available for this sheet.")
}
else
{
for(var i = 2; i < data.length; i++){
if(ss.getRange(i+1,6).getBackground !== "#d9ead3"){
if(ss.getRange(i+1,6) !== ""){
var values = ss.getRange(i+1,1,1,5).getValues();
bostab.insertRowBefore(2);
bostab.getRange(2,2,1,5).setValues(values).setFontColor("#000000");
ss.getRange(i+1,6).setBackground("#d9ead3");
}
}
Browser.msgBox("Complete")
}
}
}
The script is set to run from selecting a menu item in the "Extras" menu that is being created using the "Code.gs" script on this doc.
Modification points:
In your script, getBackground of ss.getRange(i+1,6).getBackground might be getBackground().
When getValues() and setValues() are used in a loop, the process cost will become high. Ref (Author: me)
Only column "F" is used.
When these points are reflected in your script, how about the following modification?
Modified script:
function oneFeedback() {
// Ref: https://stackoverflow.com/a/53678158
const columnIndexToLetter_ = index => (a = Math.floor(index / 26)) >= 0 ? columnIndexToLetter_(a - 1) + String.fromCharCode(65 + (index % 26)) : "";
// Retrieve source sheet.
const ss = SpreadsheetApp.getActiveSpreadsheet();
const srcSheet = ss.getSheetByName("Feedback");
if (!srcSheet) Browser.msgBox("This option isn't available for this sheet.");
// Retrieve source values.
const range = srcSheet.getDataRange();
const [header, ...values] = range.getValues();
const [, ...backgrounds] = range.getBackgrounds();
// Create an object for putting to destination sheets.
const offset = 5; // This is from your question.
const dstSheets = header.splice(offset);
const obj = dstSheets.reduce((o, e) => (o[e] = [], o), {});
const res = values.reduce((o, r, i) => {
dstSheets.forEach((h, j) => {
const idx = offset + j;
if (r[idx] == "X" && backgrounds[i][idx] != "#d9ead3") {
o[h].push(r);
o.ranges.push(`${columnIndexToLetter_(idx)}${i + 2}`);
}
});
return o;
}, { ...obj, ranges: [] });
// Put values to destination sheets.
dstSheets.forEach(e => {
const v = res[e];
if (v.length > 0) {
const dstSheet = ss.getSheetByName(e);
dstSheet.getRange(dstSheet.getLastRow() + 1, 1, v.length, v[0].length).setValues(v);
}
});
// Set background colors of source cells.
if (res.ranges.length == 0) return;
srcSheet.getRangeList(res.ranges).setBackground("#d9ead3");
}
When this script is run, I thought that your goal might be able to be achieved.
References:
reduce()
forEach()
setBackground(color) of Class RangeList

Copy data from Source sheet to Destination sheet and if the data already exists overwrite the data on Destination Sheet

The main goal is to keep track of the data updated in the Source sheet into the Destination sheet. Every week, the data from the last two weeks gets uploaded onto sheet 1. This week, we loaded weeks 46 and 45 and last week, we loaded weeks 45 and 44. Week 45 should be overwritten in the Destination Sheet with the one we uploaded this week, not the one we loaded last week. Week 46 should also go on the list. My data was transferred from Source to Destination Sheet using the following function.
function CopyDataToNewFile() {
var sss = SpreadsheetApp.openById('1BQb-_572SbReJWcD8nhfWFj8u4D15jhg7NtrsBT2vz4'); // sss = source spreadsheet
var ss = sss.getSheetByName('Source'); // ss = source sheet
var SData = ss.getDataRange().getValues();
var tss = SpreadsheetApp.openById('1BQb-_572SbReJWcD8nhfWFj8u4D15jhg7NtrsBT2vz4'); // tss = target spreadsheet
var ts = tss.getSheetByName('Destination'); // ts = target sheet
ts.getRange(ts.getLastRow()+1,1,SData.length,SData[0].length).setValues(SData);
}
I need to change the function so it overwrites week 45 too. I also want only the last 14 weeks data on sheet Destination, so I'll have to delete the weeks before the last 16 weeks. I'd appreciate your help.
Here is the link to the Google Sheet: https://docs.google.com/spreadsheets/d/1BQb-_572SbReJWcD8nhfWFj8u4D15jhg7NtrsBT2vz4/edit?usp=sharing
Try this code.
It...
looks for the latest 15 weeks of data from 'Source' sheet,
clear all contents of 'Destination' sheet,
throw the data found in step-1 into the empty 'Destination' sheet.
Note: you can change the value of the variable 'limit' to set how many weeks of data do you want to bring into your 'Destination' sheet.
If you want a target sheet to keep updated with latest data of a set period, instead of checking the whole target sheet for old data to be deleted, and try to insert new data that doesn't exists to the right place,
just delete everthing on it and re-insert the data which should stay, is always the faster, easier, and more reliable way to go.
function CopyDataToNewFile() {
const limit = 15;
const ssid = '1BQb-_572SbReJWcD8nhfWFj8u4D15jhg7NtrsBT2vz4';
const sss = SpreadsheetApp.openById(ssid);
const ss = sss.getSheetByName('Source');
const ts = sss.getSheetByName('Destination');
const sValues = ss.getDataRange().getValues();
const dates = sValues
.map(([year,week,...rest]) => [year,week].join(''))
.filter((n,i,s) => !isNaN(n) && i === s.indexOf(n))
.reverse()
.filter((v,i) => i<limit)
.map(v => [v.slice(0,4),v.slice(4)]);
const results = sValues.filter(([year,week,...rest],i) => {
return dates.some(([dYear,dWeek]) => year == dYear && week == dWeek) || i === 0;
});
ts.clearContents();
ts.getRange(1,1,results.length,results[0].length).setValues(results);
}
BUT!
If you source sheet do not always contains data of all time, that wouldn't work.
In that case, try this second attempt:
This code compares the 'year' and 'week' of 'Source' and 'Destination' sheet,
get source data that has year & week lager than those in 'Destination' sheet,
re-create values of 'Destination' sheet with the added new entries,
remove the old-values which exceeded the set 'limit' week count,
clear the target sheet,
setValues back to target sheet.
function CopyDataToNewFile() {
const limit = 15;
const getDates = (values) => {
return values
.map(([year,week,...rest]) => [year,week].join(''))
.filter((n,i,s) => !isNaN(n) && i === s.indexOf(n))
.reverse()
}
const ssid = '1BQb-_572SbReJWcD8nhfWFj8u4D15jhg7NtrsBT2vz4';
const sss = SpreadsheetApp.openById(ssid);
const ss = sss.getSheetByName('Source');
const ts = sss.getSheetByName('Destination');
const sValues = ss.getDataRange().getValues();
const tValues = ts.getDataRange().getValues(); // < typo, was ss.getDataRange(), it should be ts.getDataRange() instead.
const sDates = getDates(sValues);
const tDates = getDates(tValues);
const nDates = sDates.filter(n => n > Math.max(...tDates));
const aDates = tDates.concat(nDates).filter((val,i) => i<limit);
const results = [];
for (const [year,week,...rest] of sValues) {
if (nDates.includes([year,week].join(''))) tValues.push([year,week,...rest]);
}
for (const [i,[year,week,...rest]] of tValues.entries()) {
if (i === 0 || aDates.includes([year,week].join(''))) results.push([year,week,...rest]);
}
ts.clearContents();
ts.getRange(1,1,results.length,results[0].length).setValues(results);
}
maybe sort the date values before comparing can slove the problem, also I have rearranged some of the variables for easier access and update:
function CopyDataToNewFile2() {
const limit = 15; // limit of week counts to be returned
const ssid = '1BQb-_572SbReJWcD8nhfWFj8u4D15jhg7NtrsBT2vz4'; // id of the spreadsheet
const source = 'Source' // Source sheet name;
const destination = 'Destination' // Destination sheet name;
const getDates = (values) => {
return values
.map(([year,week,...rest]) => [year,week].join(''))
.filter((n,i,s) => !isNaN(n) && i === s.indexOf(n))
}
const sss = SpreadsheetApp.openById(ssid);
const ss = sss.getSheetByName(source);
const ts = sss.getSheetByName(destination);
const sValues = ss.getDataRange().getValues();
const tValues = ts.getDataRange().getValues(); // < typo, was ss.getDataRange(), it should be ts.getDataRange() instead.
const sDates = getDates(sValues);
const tDates = getDates(tValues);
const nDates = sDates.filter(n => n > Math.max(...tDates));
const aDates = tDates.concat(nDates).sort((a,b) => b - a).filter((val,i) => i<limit);
const results = [];
for (const [year,week,...rest] of sValues) {
if (nDates.includes([year,week].join(''))) tValues.push([year,week,...rest]);
}
for (const [i,[year,week,...rest]] of tValues.entries()) {
if (i === 0 || aDates.includes([year,week].join(''))) results.push([year,week,...rest]);
}
ts.clearContents();
ts.getRange(1,1,results.length,results[0].length).setValues(results);
}
Instead of just copy the new entries form source sheet, this code will also replace the values in target sheet if the date of given data appears in source sheet:
function CopyDataToNewSheet() {
const limit = 17; // limit of week counts to be returned
const ssid = '1Yf6RItkwa6T1pZEt-RFZRdapdy2NfoFgKUCMxqPbSD4'; // id of the spreadsheet
const source = 'Source' // Source sheet name;
const destination = 'Destination' // Destination sheet name;
const getDates = (values) => {
return values
.map(([year,week,...rest]) => [year,week].join(''))
.filter((n,i,s) => !isNaN(n) && i === s.indexOf(n))
}
const sss = SpreadsheetApp.openById(ssid); //open the spread sheet
const ss = sss.getSheetByName(source);
const ts = sss.getSheetByName(destination);
const sValues = ss.getDataRange().getValues();
const tValues = ts.getDataRange().getValues();
const sDates = getDates(sValues);
const tDates = getDates(tValues).sort((a,b) => b - a);
const oDates = tDates.filter(d => !sDates.includes(d)).filter((v,i) => i<(limit-sDates.length));
tValues.shift();
for (const [i,[year,week,...rest]] of tValues.entries()) {
if (!!i && oDates.includes([year,week].join(''))) sValues.push([year,week,...rest]);
}
const results = sValues.sort((a,b) => [b[0],b[1]].join('') - [a[0],a[1]].join(''));
ts.clearContents();
ts.getRange(1,1,results.length,results[0].length).setValues(results);
}

How can I check if a numerical value is within a range of cells in google sheets?

I would like to find if a certain value is in a range using app scripts for google sheets.
var sheet = SpreadsheetApp.getActiveSheet();
var rangeBikeNumbers = sheet.getDataRange("A5:A5000");
var values = rangeBikeNumbers.getValues();
If I have my range rangeBikeNumbers, how can I check if the number "42" for example is in that range. I have searched for hours now and have beeb unable to find any answer to this. indexOf only seems to return -1, regardless of whether or not the value is in the range.
var indexDataNumber = values.indexOf(42); for example always ends up being -1
I believe your goal as follows.
You want to check whether the value of 42 is existing in the range of A5:A5000.
In this case, I would like to propose to use TextFinder. Because when TexiFinder is used, the process cost is low. Ref By the way, getDataRange has not arguments. From your script, I thought that you might want var rangeBikeNumbers = sheet.getRange("A5:A5000");.
When this is reflected to your script, it becomes as follows.
Modified script:
function myFunction() {
var sheet = SpreadsheetApp.getActiveSheet();
var rangeBikeNumbers = sheet.getRange("A5:A5000");
var find = rangeBikeNumbers.createTextFinder("42").matchEntireCell(true).findNext();
if (find) {
// In this case, the value of 42 is existing in the range.
} else {
// In this case, the value of 42 is NOT existing in the range.
}
}
Note:
About var indexDataNumber = values.indexOf(42); for example always ends up being -1, I think that the reason of this issue is due to that values is 2 dimensional array. If you want to use this, you can also use the following script.
function myFunction() {
var sheet = SpreadsheetApp.getActiveSheet();
var rangeBikeNumbers = sheet.getRange("A5:A5000");
var values = rangeBikeNumbers.getValues();
var find = values.map(([e]) => e).indexOf(42); // of values.flat().indexOf(42);
if (find > -1) {
// In this case, the value of 42 is existing in the range.
} else {
// In this case, the value of 42 is NOT existing in the range.
}
}
References:
Benchmark: Process Costs for Searching Values in Spreadsheet using Google Apps Script
getDataRange()
getRange(a1Notation)
createTextFinder(findText)
Select any active range that you wish to search and it will search for the seed in that at range. The seed is currently defaulted to 42 but you can change it.
function findSeedInRange(seed = 42) {
const ui = SpreadsheetApp.getUi();
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
const rg = sh.getActiveRange();
const row = rg.getRow();
const col = rg.getColumn();
var found = false;
rg.getValues().forEach((r, i) => {
r.forEach((c, j) => {
if (c == seed) {
let r = sh.getRange(i + row, j + col).getA1Notation();
ui.alert(`Found ${seed} in ${r}`);
found = true;
}
})
})
if(!found) {
ui.alert(`Did not find ${seed}`);
} else {
ui.alert('That is all.')
}
}
Here's another approach:
function findSeedInRange() {
const ui = SpreadsheetApp.getUi();
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
const rg = sh.getActiveRange();
const resp = ui.prompt('Enter Seed', 'Enter Seed', ui.ButtonSet.OK_CANCEL)
if (resp.getSelectedButton() == ui.Button.OK) {
var seed = parseInt(resp.getResponseText());
const row = rg.getRow();
const col = rg.getColumn();
var found = false;
rg.getValues().forEach((r, i) => {
r.forEach((c, j) => {
if (c == seed) {
let r = sh.getRange(i + row, j + col).getA1Notation();
ui.alert(`Found ${seed} in ${r}`);
found = true;
}
});
});
if (!found) {
ui.alert(`Did not find ${seed}`);
} else {
ui.alert('That is all.')
}
} else {
ui.alert('Operation cancelled.')
}
}

Google Sheets - Modify Code To Act Dynamically When Using Slice

I am using the splice Script to run code over numerous tabs
function Slice_TABS() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets().slice(7); // get 8th sheet onwards
sheets.forEach(ss=>{
Donor();
})}
and am trying to use the below code to bring filtered results up to 10 per SKU. When i name the sheet the code works fine but i need it to run on the ActiveSpreadsheet. I am getting the following error
" Type Error: sheet.getRange is not a function" (BOLD Below)
function GetNSKUSAG11() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheets();
*//Building the matching set.
**var matchingValues = sheet.getRange('AF3:AF').getValues().filter(value => value[0] !=
'').map(nonBlankValue => [nonBlankValue[0], 0]);***
//Filtering out the desired number of values
var values = sheet.getRange("LOCATIONS!$A$3:$B").getValues().filter(value =>
advancedFilter(value[0], matchingValues));
let cols = values[0].length;
let rows = values.length;
//Printing out the found rows starting from AI
sheet.getRange(2, 36, rows, cols).setValues(values);
}
function advancedFilter(value, toMatch) {
let matched = toMatch.filter(couple => couple[0] === value);
if (matched.length > 0) {
if (matched[0][1]<10) { //Here you can decide when to stop returning SKUs
matched[0][1] += 1;
return true;
}
}
return false;
}
I think I am close.
Answer to this question - thanks to Tanaike is
Change the code as per below
var sheet = ss.getSheets()
to
var sheet = ss.getActiveSheet()

My row deletion process is working fine for a single sheet. How can it be done for all sheets of a spreadsheet having specific names?

I can delete all unprotected rows of data from a sheet using App Script. The sheet name comes from a user input of another sheet named 'Dashboard'. But now I need to delete all unprotected rows of data from all sheets of a spreadsheet having both alphabets and numbers in their names. Please note that I have two types of sheet names like CSE4115, ICT1234, MATH4101, etc. (both alphabets and number) and also Master, Dashboard, TempDataSet, etc. (only alphabets in their names). So I need to do deleting all unprotected rows of data from sheets like CSE4115, ICT1234, MATH4101 and so on. No user input is required I think.
My codes are as follows:
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Tasks')
.addItem('Data Cleansing', 'dataCleansing')
.addSeparator()
.addToUi();
}
function dataCleansing(){
var ss = SpreadsheetApp.getActive();
var allsheets = ss.getSheets();
for(var s in allsheets){
var sheet = allsheets[s];
if((sheet.indexOf("0")>-1 || sheet.indexOf("1")>-1 || sheet.indexOf("2")>-1 ||
sheet.indexOf("3")>-1 || sheet.indexOf("4")>-1 || sheet.indexOf("5")>-1 ||
sheet.indexOf("6")>-1 || sheet.indexOf("7")>-1 || sheet.indexOf("8")>-1 || sheet.indexOf("9")>-1)){
//For removing unprotected rows of data from a particular sheet
// 1. Retrieve data range.
const dataRange = sheet.getDataRange();
// 2. Create an object from the protected range. This is used for removing from the cleared rows.
const protectedRanges = sheet.getProtections(SpreadsheetApp.ProtectionType.RANGE).map(e => {
const r = e.getRange();
const start = r.getRow();
return {start: start, end: r.getNumRows() + start - 1};
});
// 3. Create range list for clearing rows using the object.
let rangeList = [];
for (let r = 2; r <= dataRange.getNumRows(); r++) {
let bk = false;
for (let e = 0; e < protectedRanges.length; e++) {
if (protectedRanges[e].start == r) {
r = protectedRanges[e].end;
bk = true;
break;
}
}
if (!bk) rangeList.push(`A${r}:${r}`);
}
// 4. Delete the rows without the rows of the protected ranges.
if (rangeList.length > 0) sheet.getRangeList(rangeList).getRanges().reverse().forEach(r => sheet.deleteRow(r.getRow()));
}
}
}
Untested, but I think you should change this bit:
var allsheets = ss.getSheets();
to:
const allsheets = ss.getSheets().filter(sh => /\d/.test(sh.getName()));
which basically says "get all sheets which contain a number/digit (0-9) in their name)" and lets you get rid of the IF check below:
if((sheet.indexOf("0")>-1 || sheet.indexOf("1")>-1 || sheet.indexOf("2")>-1 ||
sheet.indexOf("3")>-1 || sheet.indexOf("4")>-1 || sheet.indexOf("5")>-1 ||
sheet.indexOf("6")>-1 || sheet.indexOf("7")>-1 || sheet.indexOf("8")>-1 || sheet.indexOf("9")>-1))
Unrelated:
You use a for-in loop to iterate over allsheets, but since allsheets should be an array, it might be better to use for-of. In other words: for (const sheet of allsheets) { ... }
Is A${r}:${r} valid or does it need a column reference after the :? Not sure, just pointing it out in case.