Remove multiple columns from Google Sheets avoiding expensive loop - javascript

I am trying to avoid the following code because it is too slow:
for (var c = 25; c>2; c--){
if (sheet2.getRange(1,c).getValue() == 0)
{sheet2.deleteColumn(c)}
}
Instead I tried to find a list of columns I want to delete from the array and then set the array. (I recently figure out that deleting rows/columns in a loop is very expensive: google script loop performance)
I found this Removing columns of data in javascript array and try to apply it to my code, but it is not working.
Here is the code.
var ary = sheet2.getRange(2,1,outData.length+1,outData[0].length).getValues();
var indexesToRemove = [];
for (var c = 25; c>2; c--){
if (sheet2.getRange(1,c).getValue() == 0)
{
indexesToRemove.push(c);
}
}
The part above works well. What is not working is the function to remove the columns from the array once I found the indexes to remove. The array _row is not what I am looking for. What am I doing wrong?
removeColumns(ary, indexesToRemove);}
function removeColumns(data, indexes) {
return data.map(function (row) {
// when we remove columns, the indexing gets off by 1 each time, keep track of how many to adjust
var indexAdjustment = 0;
// copy row w/ .slice so we do not modify the original array
var _row = row.slice();
indexes.forEach(function (colIndex) {
// remove column
_row.splice(colIndex - indexAdjustment, 1);
// add 1 to adjustment to account for the column we just removed
indexAdjustment++
});
return _row;
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet2 = ss.getSheetByName('Cache');
sheet2.clear();
sheet2.getRange(2,1,_row.length,_row[0].length).setValues(_row);
});
}
BTW, I have also tried this before, but still not working:
var ary = sheet2.getRange(2,1,outData.length+1,outData[0].length).getValues();
for (var c = 25; c>2; c--){
if (sheet2.getRange(1,c).getValue() == 0)
{ ary = ary.map(function(item){
return item.splice(0,c)});
}
}

You want to delete the columns that the value of is 0 in the cells C1:Y1.
You want to reduce the process cost of the script.
You want to achieve this without using Sheets API.
Pattern 1:
In this pattern, at first, the cells which have the value of 0 from the cells C1:Y1 using TextFinder, and the columns are deleted from the retrieved cells using deleteColumn().
Sample script:
const sheet = SpreadsheetApp.getActiveSheet();
sheet.getRange("C1:Y1")
.createTextFinder(0)
.matchEntireCell(true)
.findAll()
.reverse()
.forEach(e => sheet.deleteColumn(e.getColumn()));
Pattern 2:
In this pattern, at first, all values are retrieved from "C1" to the last column for the all data rows, and delete the columns in the array and clear the range, and then, the values are put to the sheet. The method for directly processing the retrieved values has already been proposed. So as other pattern, I proposed the method which uses the transpose.
Sample script:
const sheet = SpreadsheetApp.getActiveSheet();
const range = sheet.getRange(1, 3, sheet.getLastRow(), sheet.getLastColumn() - 2);
const values = range.getValues();
const t = values[0].reduce((ar, r, i) => {
if (r != 0) ar.push(values.map(c => c[i]));
return ar;
}, []);
const v = t[0].map((_, i) => t.map(c => c[i]));
range.clearContent();
sheet.getRange(1, 3, v.length, v[0].length).setValues(v);
Pattern 3:
In this pattern, the request body for the batchUpdate method of Sheets API is created using the 1st row values, and the request body is used for requesting to Sheets API. By this, several columns can be deleted by one API call.
Before you run the script, please enable Sheets API at Advanced Google services.
Sample script:
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.getActiveSheet();
const sheetId = sheet.getSheetId();
// Create rerequests for DeleteDimensionRequest.
const requests = sheet.getRange("C1:Y1")
.createTextFinder(0)
.matchEntireCell(true)
.findAll()
.reverse()
.map(e => {
const col = e.getColumn();
return {deleteDimension: {range: {sheetId: sheetId, dimension: "COLUMNS", startIndex: col - 1, endIndex: col}}}
});
// Request to the batchUpdate method using the request body.
Sheets.Spreadsheets.batchUpdate({requests: requests}, spreadsheet.getId());
In this case, requests is created using the method of pattern 1. Each request is as follows. You can see about this structure at the document.
{
"deleteDimension": {
"range": {
"sheetId": "###",
"dimension": "COLUMNS",
"startIndex": ##,
"endIndex": ##
}
}
}
References:
Class TextFinder
Advanced Google services
Method: spreadsheets.batchUpdate
DeleteDimensionRequest

function runOne() {
var d=0;
var ss=SpreadsheetApp.getActive();
var sh=ss.getActiveSheet();
var hA=sh.getRange(1,1,1,sh.getLastColumn()).getValues()[0];//header array
var vs=sh.getRange(2,1,sh.getLastRow()-1,sh.getLastColumn()).getValues();//data array
vs.forEach(function(r,j){
var d=0;
hA.forEach(function(h,i){
if(h==0)r.splice(i-d++,1);//removes elements in columns whose headers are == 0
});
});
Logger.log(vs);
}

Try using Sheets Advanced Service and batchUpdate.
Related
Remove all grouped rows / columns in a spreadsheet
Reference
https://developers.google.com/sheets/api/samples/rowcolumn#delete_rows_or_columns

Related

How do I iterate through my google sheets rows, update an array, and show results in another sheet?

I am trying to write a Google Apps Script function which scans every row of my spreadsheet, and if column 36 has a specific string, will look at column 31, column 23, etc. of that row to create an array with the data in these columns.
I think it will ultimately require an array updating function inside an if statement inside a loop.
Loop through every row of column 36
If string matches target string
Add row's data to an array
Update another sheet with data from this array
Thank you!
Here is what I've tried so far:
`
function myFunction() {
var sheet = SpreadsheetApp.getActive().getSheetByName("Database");
var range = sheet.getRange(2, 36).getValue();
Logger.log(range);
}
`
Here is the code, replace the string of Target Sheet Name, the specific String and etc on your own.
function myFunction() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
// DataBase sheet:
const dbSheet = ss.getSheetByName("Database");
// Target sheet for placing your outputs:
const tgSheet = ss.getSheetByName("Target Sheet Name");
// Get all values as a 2D array from DataBase sheet:
const sourceValues = dbSheet.getDataRange().getValues();
// Declare result array.
const results = [];
// Loop through every row:
for (const row of sourceValues) {
const specificString = 'Your String';
// check the value of column 36 of each row.
const check = row[36] === specificString;
// if condition check returns true, form an array with column 31, column 23 and 'etc' of this row, than push it into the result array.
if(check) results.push([row[31],row[23],'etc']);
}
// print the result onto your Target Sheet, start from A1.
tgSheet.getRange(1,1,results.length,results[0].length).setValues(results);
}
Move selected rows to another sheet
function elfunko(specificvalue) {
const ss = SpreadsheetApp.getActive();
const vs = ss.getSheetByName("Database").getDataRange().getValues().filter(r => r[35] == specificvalue);
const sh = ss.getSheetByName("Sheet1");
sh.getRange(sh.getLastRow() + 1, 1, vs.length, vs[0].length).setValues(vs);
}

Use Cell Range instead of A1-notation in Google Sheets

I am using the Google API to fetch data from a Google Spreadsheet via Node.js.
In Excel VBA it is possible to either use
Range("A3") or Cells(3,1)
to reference the range of the cell.
And for a multiple cell range it would be like:
Range(Cells(1,1), Cells(2,3))
instead of:
Range("A1:C2")
I could not find out how to do that via the Google Sheets API.
Tried following this guideline, but I think it's not the thing I need here:
https://googlesheets4.tidyverse.org/articles/range-specification.html
My example code in Node.js looks like this:
async function getHelperData(cl){
const gsapi = google.sheets({version:'v4', auth: cl});
const opt = {
spreadsheetId: gs.gs_ID,
range: '_helperSheet!A1:C2'
};
let data = await gsapi.spreadsheets.values.get(opt);
let dataArray = data.data.values;
return dataArray;
};
Is it actually possible?
About Use Cell Range instead of A1-notation in Google Sheets and the use of a value like Range(Cells(1,1), Cells(2,3)), I thought that in the current stage, when Sheets API is used, the a1Notation is used for retrieving values from Google Spreadsheet. So, in this case, I thought that it is required to convert a value like Range(Cells(1,1), Cells(2,3)) to the a1Notation.
When this is reflected in your script, how about the following modification?
Modified script:
const gsapi = google.sheets({version:'v4', auth: cl});
const sheetName = "_helperSheet"; // Please set sheet name.
const rangeObj = { start: [1, 1], end: [2, 3] }; // This is from `Range(Cells(1,1), Cells(2,3))` of "A1:C2" in your question.
const columnIndexToLetter_ = (index) => (a = Math.floor(index / 26)) >= 0 ? columnIndexToLetter_(a - 1) + String.fromCharCode(65 + (index % 26)) : ""; // Ref: https://stackoverflow.com/a/53678158
const a1Notation = rangeObj.end ? `'${sheetName}'!${columnIndexToLetter_(rangeObj.start[1] - 1)}${rangeObj.start[0]}:${columnIndexToLetter_(rangeObj.end[1] - 1)}${rangeObj.end[0]}` : `'${sheetName}'!${columnIndexToLetter_(rangeObj.start[1] - 1)}${rangeObj.start[0]}`;
const opt = {
spreadsheetId: gs.gs_ID,
range: a1Notation,
};
let data = await gsapi.spreadsheets.values.get(opt);
let dataArray = data.data.values;
console.log(dataArray);
In this modification, it supposes that the value of gs.gs_ID has already been declared elsewhere. Please be careful about this.
In this modification, a1Notation returns '_helperSheet'!A1:C2.
If const rangeObj = { start: [1, 1] }; is used, a1Notation returns '_helperSheet'!A1.

Google Sheets, stack report from multiple workbooks

Goal: To stack data from 90+ google workbooks, all with the same sheet name, into the one master sheet for reporting
Info:
All worksheets have the same number of columns.
I have the following script but it does not run properly, I think the issue is with how I am caching / Pushing the data to the array before pasting to the output sheet.
I am trying to build an array then paste it in one go.
The tables I am stacking have 47 columns, unknown number of rows.
The part that opens the sheets is all working perfectly.
// Get the data from the worksheets
var indexsheet = SpreadsheetApp.getActive().getSheetByName("Index");
var outputsheet = SpreadsheetApp.getActive().getSheetByName("Output");
var response = SpreadsheetApp.getUi().prompt('Current Cycle', 'Enter Cycle Name Exactly in YY-MMM-Cycle# format', SpreadsheetApp.getUi().ButtonSet.OK_CANCEL)
var CurrentCycleName = response.getResponseText()
// Assign datasets to variables
var indexdata = indexsheet.getDataRange().getValues();
// For each workbook in the index sheet, open it and copy the data to a cache
indexdata.forEach(function(row, r) {
try {
//open Entity specific workbook
var workbookid = indexsheet.getRange(r + 1, 7, 1, 1).getValues();
var Entityworkbook = SpreadsheetApp.openById(workbookid)
// Open workhseet
Entitysheet.getSheetByName(CurrentCycleName)
// Add PR Data to cache - stacking for all countrys
var PRDataCache = Entitysheet.getDataRange().push()
} catch {}
})
// Set the all values of the sheet at once
outputsheet.getRange(r + 1, 14).setValue('Issue Splitting Data')
Entitysheet.getRange(2, 1, PRDataCache.length || 1, 47).setValues(PRDataCache)
};
This is the index tab where we are getting the workbookid from to open each file
This is the output file, we are stacking all data from each country
I believe your goal is as follows.
You want to retrieve the Spreadsheet IDs from the column "G" of "Index" sheet.
You want to give the specific sheet name using a dialog.
You want to retrieve all values from the specification sheet in all Spreadsheets. In this case, you want to remove the header row.
You want to put the retrieved values on "Output" sheet.
In this case, how about the following sample script?
Sample script:
function myFunction() {
var ss = SpreadsheetApp.getActive();
var indexsheet = ss.getSheetByName("Index");
var outputsheet = ss.getSheetByName("Output");
var response = SpreadsheetApp.getUi().prompt('Current Cycle', 'Enter Cycle Name Exactly in YY-MMM-Cycle# format', SpreadsheetApp.getUi().ButtonSet.OK_CANCEL);
var CurrentCycleName = response.getResponseText();
var ids = indexsheet.getRange("G1:G" + indexsheet.getLastRow()).getValues();
var values = ids.reduce((ar, [id]) => {
try {
var [, ...values] = SpreadsheetApp.openById(id).getSheetByName(CurrentCycleName).getDataRange().getValues();
ar = [...ar, ...values];
} catch (e) {
console.log(`"${id}" was not found.`);
}
return ar;
}, []);
if (values.length == 0) return;
// If the number of columns is different in all Spreadsheets, please use the following script.
// var maxLen = Math.max(...values.map(r => r.length));
// values = values.map(r => r.length < maxLen ? [...r, ...Array(maxLen - r.length).fill("")] : r);
outputsheet.getRange(outputsheet.getLastRow() + 1, 1, values.length, values[1].length).setValues(values);
}
Note:
When the number of Spreadsheet IDs is large, the processing time might be over 6 minutes. I'm worried about this. At that time, how about separating the Spreadsheet IDs?
Reference:
reduce()

Move Specific Rows depending on Filtering Keywords within unknown amount of rows using Google Sheets Apps Scripts

I do SEO, and therefore I have a lot of keywords flowing around in different spreadsheets. I'd like a way to filter these into seperate sheets based on specific filters, but I can't for the life of me, figure out how to do this in Google Apps Script.
Criteria I set myself for this to work out:
A list of strings and their corresponding volumes are entered in column 1+2.
A list of filter-words are written in column 3.
The script has to create a new sheet for each of the filter words and move the strings + volumes into these different sheets if the string contains a filter word.
Example:
Filter words: Apple, Banana, Pineapple
String: "The Apple Was Big", Volume: "100"
The script would move the string and volume into the sheet called "Apple" on row 1
(Beware, I'm in no means experienced in coding)
I believe you can use the following structure:
for(let i = 0; i <= column3RowAmount; i++){ //Run as long as there are more filter words
create(column3Row[i]); //create a new sheet with the name of the filter word
for(let j = 0; j <= column1RowAmount; j++){ //Run as long as there are more keywords
if(column1Row[j].indexOf(column3Row[i]) >= 0){ //If the Row in column 1 contains the filter word
column1Row[j].moveToSheet(column3Row[i]); // Make sure not to move Column 3, but only 1+2
}
}
}
Example sheet: https://docs.google.com/spreadsheets/d/15YIMyGmmfZdy094gwuJNxFmTd8h7NOLnA8KevZrGtdU/edit?usp=sharing
Explanation:
Your goal is to create a sheet for every filter-word in column C. Then copy the data in columns A, B but only the rows that include the filter-word to the corresponding sheet.
For starters, you need to get the filter-word list. You can get the full range of column C and filter out the empty cells:
const sh_names = sh.getRange('C1:C').getValues().flat().filter(r=>r!='');
Similarly, you need to get the data in columns A and B:
const data = sh.getRange('A1:B'+sh.getLastRow()).getValues();
The next step is to iterate over sh_names and for every element / filter-word, check if a sheet with that name exists. If it does not exist, then create a sheet with that name, if it exists then skip the creation part:
if(!ss.getSheetByName(s)){
ss.insertSheet().setName(s);}
The next step is to filter data on the rows that include the filter-word:
let f_data = data.filter(r=>r[0].includes(s));
Finally, check if the length of the data is bigger than 0, otherwise there is not data to use and set the values of data to the corresponding sheet:
sheet.getRange(sheet.getLastRow()+1,1,f_data.length,f_data[0].length).setValues(f_data)
Solution
function myFunction() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Ark1');
const filter_sh = ss.getSheetByName('Filter');
const data = sh.getRange('A1:B'+sh.getLastRow()).getValues();
const sh_names = filter_sh.getRange('A1:A'+filter_sh.getLastRow()).getValues().flat();
sh_names.forEach(s=>{
if(!ss.getSheetByName(s)){
ss.insertSheet().setName(s);}
let sheet = ss.getSheetByName(s);
let f_data = data.filter(r=>r[0].includes(s));
if(f_data.length>0){
sheet.getRange(sheet.getLastRow()+1,1,f_data.length,f_data[0].length).setValues(f_data);}
});
}
This function will place all of your results into column 4 next to the appropriate word rather than creating a page for each word. So it runs much faster.
function stringswords() {
const ss=SpreadsheetApp.getActive();
const sh=ss.getSheetByName('Sheet1');
const sr=2;
const rgd=sh.getRange(sr,1,sh.getLastRow()-sr+1,2);
const data=rgd.getDisplayValues();
const rgw=sh.getRange(sr,3,sh.getLastRow()-sr+1,1);
const words=rgw.getDisplayValues().flat();
const wiObj={};
words.forEach(function(w,i){wiObj[w]=i});
const rgr=sh.getRange(sr,4,sh.getLastRow()-sr+1,1);
rgr.clearContent();
var results=rgr.getValues();
words.forEach(function(w,i,A){
data.forEach(function(r,j,D) {
if(data[j][0] && data[j][0].indexOf(w)!=-1) {
results[wiObj[w]][0]+=Utilities.formatString('String:%s Vol:%s\n',data[j][0],data[j][1]);
}
});
});
rgr.setValues(results);
}
Image of Data and output:

How to compare two sheets and delete/add any column with a distinct value in row 1? Google Script

I want to compare two sheets (based on header values in row 1) and delete any column with a unique value (without a match). For example, Assuming Sheet1, Row 1 data and Sheet 2, Row 1 are uniform, if a user adds/deletes a column within any sheet, I want to always match the number of columns in both sheets with their values
Screenshots of sheets headings.
IF both sheets looks like this
And a user adds a new Column N
Or delete column N
How can I ensure that both sheet matches by deleting the odd/distinct column in Sheet 1?
I have tried modifying this code below but I can't just get the unique one out. This code only look for headers with a defined value.
function deleteAloneColumns(){
var sheet = SpreadsheetApp.getActiveSheet();
var lastColumnPos = sheet.getLastColumn();
var headers = sheet.getRange( 1 ,1, 1, lastColumnPos ).getValues()[0];
for( var i = lastColumnPos ; i < 1; i--){
if( headers[i] === "alone" ) sheet.deleteColumn(i);
}
SpreadsheetApp.getUi().alert( 'Job done!' );
}
Any help to compare and delete the column with the unique value will be appreciated.
Problem
Balancing sheets based on header row values mismatch.
Solution
If I understood you correctly, you have a source sheet against which validation is run and two primary use cases: user adds a new column named differently than any other column (if you want to check that the column strictly matches the one in sheet1, it is easy to modify) in source sheet or deletes one that should be there.
const balanceSheets = (sourceShName = 'Sheet1',targetShName = 'Sheet2') => {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const s1 = ss.getSheetByName(sourceShName);
const s2 = ss.getSheetByName(targetShName);
const s2lcol = s2.getLastColumn();
//keep all vals from source to reduce I/O
const s1DataVals = s1.getDataRange().getValues();
const s2Vals = s2.getRange(1, 1, 1, s2lcol).getValues();
const h1Vals = s1DataVals[0];
const h2Vals = s2Vals[0];
//assume s1 is source (validation) sheet
//assume s2 is target sheet that a user can edit
//case 1: target has value not present in source -> delete column in target
let colIdx = 0;
h2Vals.forEach(value => {
const isOK = h1Vals.some(val => val===value);
isOK ? colIdx++ : s2.deleteColumn(colIdx+1);
});
//case 2: target does not have values present in source -> append column from source
h1Vals.forEach((value,index) => {
const isOK = h2Vals.some(val => val===value);
!isOK && s2.insertColumnAfter(index);
const valuesToInsert = s1DataVals.map(row => [row[index]]);
const numRowsToInsert = valuesToInsert.length;
s2.getRange(1,index+1, numRowsToInsert,1).setValues(valuesToInsert);
});
};
Showcase
Here is a small demo of how it works as a macros:
Notes
Solving your problem with two forEach is suboptimal, but I kept number of I/O low (it can be lowered further by, for example, moving deleteColum out of the loop while only keeping track of column indices).
The script uses ES6 capabilities provided by V8, so please, be careful (although I would recommend migrating as soon as possible - even if you encounter bugs / inconsistencies , it is worth more than it costs.
UPD made script more flexible by moving sheet names to parameter list.
UPD2 after discussing the issue with deleteColumn() behaviour, the answer is updated to keep column pointer in bounds (for those curious about it - forEach kept incrementing the index, while deleteColumn reduced bounds for any given index).
Reference
insertColumnAfter() method reference

Categories

Resources