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
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);
}
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.')
}
}
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()
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.